<?php

/**
 * @copyright Nichlas Severinsen
 * @license   https://opensource.org/licenses/BSD-2-Clause
 */

namespace Necklace\XxHash;

use Exception;
use FFI;

abstract class XxHash
{
	/**
	 * Foreign Function Interface.
	 *
	 * @var FFI
	 */
	protected $ffi = null;

	/**
	 * XXH State pointer.
	 *
	 * @var FFI\CData
	 */
	protected $state = null;

	/**
	 * XXH Version number.
	 *
	 * @var int
	 */
	protected $version = null;

	/**
	 * Constructor.
	 */
	public function __construct()
    {
		$this->ffi = FFI::load(__DIR__ . '/../include/xxhash-php.h');

		$this->version = $this->ffi->XXH_versionNumber();
	}

	/**
	 * Convert XXH32_canonical_t to PHP string.
	 *
	 * @param  FFI\CData $canonical
	 * @return string
	 */
	protected function canonicalToString(FFI\CData $canonical): string
	{
		$str = '';

		foreach($canonical->digest as $int)
		{
			$str .= sprintf('%02x', $int);
		}

		return $str;
	}

	/**
	 * Print XXH Version in readable format.
	 *
	 * @return string
	 */
	public function version(): string
	{
		return sprintf('%s.%s.%s', round($this->version / 100 / 100), round($this->version / 100), $this->version % 100);
	}

	/**
	 * Simple file hashing function.
	 *
	 * @param  string $filename  Filename or filepath.
	 * @param  int    $seed      Seed used to alter the result.
	 * @param  string $secret    Secret used to alter the result.
	 * @param  int    $blockSize Size of blocks to read at a time.
	 * @return string
	 */
	public function hashFile($filename, $seed = null, $secret = null, $blockSize = 64 * 1024): string
	{
		// Check if file actually exists and is readable.

		if(!is_readable($filename))
		{
			throw new Exception(sprintf('File %s either does not exist or is not readable', $filename));
		}

		// Reset state with seed.

		$this->reset($seed, $secret);

		// Read file

		$handle = fopen($filename, 'r');

		if($handle)
		{
			while($data = stream_get_line($handle, $blockSize))
			{
				$this->update($data);
			}
			fclose($handle);
		}

		return $this->digest();
	}

	/**
	 * Hash some data.
	 *
	 * @param  string $data   Data you want to hash.
	 * @param  int    $seed   Seed used to alter the result.
	 * @param  string $secret Secret used to alter the result.
	 * @return string
	 */
	abstract public function hash(string $data, int $seed = null, string $secret = null);

	/**
	 * Starts a new hash by initializing state (with or without seed).
	 *
	 * @param  int    $seed   Seed used to alter the result.
	 * @param  string $secret Secret used to alter the result.
	 * @return bool
	 */
	abstract public function reset(int $seed = null, string $secret = null);

	/**
	 * Feed the hash state with data.
	 *
	 * @param  string $data Feeding data.
	 * @return bool
	 */
	abstract public function update(string $data);

	/**
	 * Produce a hash from the fed data.
	 *
	 * @return string
	 */
	abstract public function digest();
}
